记事本WriteFile() API钩取

1、调试技术流程

具体调试流程如下:

1、对想钩取的进程进行附加操作,使之成为被调试者;
2、“钩子”:将API起始地址的第一个字节修改为0xCC;
3、调用相应API时,控制权转移到调试器;
4、执行需要的操作(操作参数、返回值等);
5、脱钩:将0xCC恢复原值;
6、运行相应API(无0xCC的正常状态);
7、“钩子”:再次修改为0xCC(为了继续钩取);
8、控制器返还被调试者。

以上是最简单的情形,在此基础上可以有多种变化。

2、记事本WriteFile() API钩取

首先运行Notepad.exe,获取其PID。
运行钩取程序(hookdbg.exe)。hookdbg.exe是基于控制台的程序,其运行参数为目标进程的PID,运行hookdbg.exe程序后,就开始了对notepad进程的WriteFile() API的钩取,如图所示。
GZoaVS.png
然后在notepad中随意输入一下英文小写字母,如图所示。
GZTWOP.png
完成输入后保存,notepad界面中不会有任何变化。关闭notepad,查看hookdbg程序的控制台窗口,如图所示。
GZ7BXq.png
打开保存的txt文件,查看实际文本是以大写字母形式保存。

3、工作原理

WriteFile()定义:

1
2
3
4
5
6
7
BOOL WriteFile(
HANDLE hFile,
LPCVOID lpBuffer, //数据缓冲区指针
DWORD nNumberOfBytesToWrote, //要写的字节数
LPDWORD lpNumberOfBytesWritten,
LPOVERLAPPED lpOverlapped
);

使用OllyDbg打开notepad后,在Kernel32!WriteFile() API处设置断点,按(F9)键运行程序。在记事本中输入文本后,以合适的文件名保存,在OllyDbg代码窗口中可以看到,调试器在kernel32!WriteFile()处暂停,然后查看进程,发现当前栈中存在一个返回值,ESP+8中存在数据缓冲区的地址。直接转到数据缓冲区地址处,可以看到要保存到notepad的字符串。钩取WriteFile() API后,用指定字符串覆盖数据缓冲区中的字符串即可达成所愿。

4、源代码分析

1、main()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main(int argc, char * argv[])
{
DWORD dwPID;

if(argc != 2)
{
printf("\nUSAGE : hookdbg.exe <pid>\n");
return 1;
}

dwPID = atoi(argv[1]);
if(!DebugActiveProcess(dwPID))
{
printf("DebugActiveProcess(%d) failed!!!\n"
"Error Code = %d\n", dwPID, GetLastError());
return 1;
}

DebugLoop();

return 0;
}

main()函数以程序运行参数的形式接收要钩取API的进程的PID,然后通过DebugActiveProcess() API将调试器附加到该运行的进程上,开始调试。

1
2
3
BOOL WINAPI DebugActiveProcess(
DWORD dwProcessId
);

然后进入DebugLoop()函数,处理来自被调试者的调试事件。

2、DebugLoop()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void DebugLoop()
{
DEBUG_EVENT de;
DWORD dwContinueStatus;
//等待被调试者发生事件
while(WaitForDebugEvent(&de, INFINITE))
{
dwContinueStatus = DBG_CONTINUE;
//被调试进程生成或者附加事件
if(CREATE_PROCESS_DEBUD_EVENT == de.dwDebugEventCode)
{
OnCreateProcessDebugEvent(&de);
}
//异常事件
else if( EXCEPTION_DEBUG_EVENT == de.dwDebugEventCode )
{
if( OnExceptionDebugEvent(&de) )
continue;
}
//被调试进程终止事件
else if( EXIT_PROCESS_DEBUG_EVENT == de.dwDebugEventCode )
{
break;
}
//再次运行被调试者
ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwContinueStatus);
}
}

DebugLoop()函数的工作原理类似于窗口过程函数,它从被调试者处接收事件并处理,然后被调试事件者继续运行。
WaitForDebugEvent() API是一个等待被调试者发生调试事件的函数。

1
2
3
4
BOOL WINAPI WaitForDebugEvent(
LPDEBUG_EVENT lpDebugEvent,
DWORD dwMilliseconds
);

DebugLoop()函数代码中,若发生调试事件,WaitForDebugEvent() API就会将相关事件信息设置到其第一个参数的变量(DEBUG_EVENT结构体对像),然后立刻返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _DEBUG_EVENT { // de  
DWORD dwDebugEventCode;
DWORD dwProcessId;
DWORD dwThreadId;
union {
EXCEPTION_DEBUG_INFO Exception;
CREATE_THREAD_DEBUG_INFO CreateThread;
CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
EXIT_THREAD_DEBUG_INFO ExitThread;
EXIT_PROCESS_DEBUG_INFO ExitProcess;
LOAD_DLL_DEBUG_INFO LoadDll;
UNLOAD_DLL_DEBUG_INFO UnloadDll;
OUTPUT_DEBUG_STRING_INFO DebugString;
RIP_INFO RipInfo;
} u;

} DEBUG_EVENT;

ContinueDebugEvent() API是一个使被调试者继续运行的函数。

1
2
3
4
5
6
7
8
BOOL ContinueDebugEvent(
DWORD dwProcessId,
// process to continue
DWORD dwThreadId,
// thread to continue
DWORD dwContinueStatus
// continuation status
);

DebugLoop()函数处理三种调试事件,如下所示。

1、EXIT_PROCESS_DEBUG_EVENT
2、CREATE_PROCESS_DEBUG_EVENT
3、EXCEPTION_DEBUG_EVENT

3、EXIT_PROCESS_DEBUG_EVENT

被调试进程终止时会插发该事件。

4、CREATE_PROCESS_DEBUG_EVENT(OnCreateProcessDebugEvent())

OnCreateProcessDebugEvent()是CREATE_PROCESS_DEBUG_EVENT事件句柄,被调试进程启动时即调用执行该函数。

1
2
3
4
5
6
7
8
9
10
11
12
BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pde)
{
//获取WriteFile() API地址
g_pfWriteFile = GetProcAddress(GetModuleHandleA("kernel32.dll"), "WriteFile");

//更改第一个字节为0xCC(INT3),originalbyte是g_ch0rgByte备份
memcpy(&g_cpdi, &pde->u.CreateProcessInfo, sizeof(CREATE_PROCESS_DEBUG_INFO));
ReadProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_OrgByte, sizeof(BYTE), NULL);
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chINT3, sizeof(BYTE), NULL);

return TRUE;
}

首先获取WriteFile() API的起始地址,它获取的不是被调试进程的内存地址,而是调试进程的内存地址。对于Windows OS的系统而言,它们在所有进程中都会加载到相同地址。
g_cpdi是CREATE_PROCESS_DEBUG_INFO结构体变量。

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct _CREATE_PROCESS_DEBUG_INFO { // cpdi  
HANDLE hFile;
HANDLE hProcess;
HANDLE hThread;
LPVOID lpBaseOfImage;
DWORD dwDebugInfoFileOffset;
DWORD nDebugInfoSize;
LPVOID lpThreadLocalBase;
LPTHREAD_START_ROUTINE lpStartAddress;
LPVOID lpImageName;
WORD fUnicode;
} CREATE_PROCESS_DEBUG_INFO;

通过CREATE_PROCESS_DEBUG_INFO结构体的hProcess成员,可以钩取WriteFile() API。
由于调试器拥有被调试进程的句柄,所以可以使用ReadProcessMemory()、WriteProcessMemory() API对被调试进程的内存空间自由进行读写操作。通过ReadProcessMemory()读取WriteFile() API的第一个字节,将其保存到g_chOrgByte变量,后面脱钩会用到。然后用WriteProcessMemory() API的第一个字节更改为0xCC,将控制权转移到调试器。

5、EXCEPTION_DEBUG_EVENT(OnExceptionDebugEvent())

OnExceptionDebugEvent()是EXCEPTION_DEBUG_EVENT事件句柄,它处理被调试者的INT3指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
BOOL OnExceptionDebugEvent(LPDEBUG_EVENT pde)
{
CONTEXT ctx;
PBYTE lpBuff = NULL;
DWORD dwNumOfBytesToWrite, dwAddress;
PEXCEPTION_RECORD per = &pde->u.Exception.ExpectionRecord;

//断点异常时
if(EXCEPTION_BREAKPOINT == per->ExceptionCode)
{
//断点地址为WriteFile() API地址时
if(g_pfWriteFile == per->ExceptionAddress)
{
//Unhook,将0xCC恢复为original byte
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chOrgByte, sizeof(BYTE), NULL);
//获取线程上下文
ctx.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(g_cpdi.hThead, &ctx);

//获取WriteFile()的param 2、3值,param 2 = ESP + 0x8、param 3 = ESP + 0xC
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0x8), &dwAddrOfBuffer, sizeof(DWORD), NULL);
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp +0xC), &dwNumOfBytesToWrite, sizeof(DWORD), NULL);

//分配临时缓冲区
lpBuffer = (PBYTE)malloc(dwNumOfBytesToWrite+1);
memset(lpBuffer, 0, dwNumOfBytesToWrite+1);

//复制WriteFile()缓冲区到临时缓冲区
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,
lpBuffer, dwNumOfBytesToWrite, NULL);
printf("\n### original string ###\n%s\n", lpBuffer);

//将小写字母转换为大写字母
for( i = 0; i < dwNumOfBytesToWrite; i++ )
{
if( 0x61 <= lpBuffer[i] && lpBuffer[i] <= 0x7A )
lpBuffer[i] -= 0x20;
}

printf("\n### converted string ###\n%s\n", lpBuffer);

//将变换后的缓冲区复制到WriteFile()缓冲区
WriteProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,
lpBuffer, dwNumOfBytesToWrite, NULL);

//释放临时缓冲区
free(lpBuffer);

//将线程上下文的EIP更改为WriteFile()的首地址
ctx.Eip = (DWORD)g_pfWriteFile;
SetThreadContext(g_cpdi.hThread, &ctx);

//运行被调试进程 ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_CONTINUE);
Sleep(0);

//API"钩子"
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
&g_chINT3, sizeof(BYTE), NULL);

return TRUE;
}
}

return FALSE;
}

1、“脱钩”
首先需要“脱钩”,在将小写字母转换为大写字母后需要正常调用WriteFile()函数。

1
2
//Unhook,将0xCC恢复为original byte
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chOrgByte, sizeof(BYTE), NULL);

2、获取线程上下文
再次运行先前线程时,必须有运行所需的信息,这些重要信息指的就是CPU中各寄存器的值。通过这些值,才能保证CPU能够再次准确运行它。负责保存CPU寄存器信息的就是CONTEXT结构体,它的定义如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
typedef struct _CONTEXT
{
DWORD ContextFlags // -| +00h
DWORD Dr0 // | +04h
DWORD Dr1 // | +08h
DWORD Dr2 // >调试寄存器 +0Ch
DWORD Dr3 // | +10h
DWORD Dr6 // | +14h
DWORD Dr7 // -| +18h

FLOATING_SAVE_AREA FloatSave; //浮点寄存器区 +1Ch~~~88h

DWORD SegGs //-| +8Ch
DWORD SegFs // |\段寄存器 +90h
DWORD SegEs // |/ +94h
DWORD SegDs //-| +98h

DWORD Edi //________ +9Ch
DWORD Esi // | 通用 +A0h
DWORD Ebx // | 寄 +A4h
DWORD Edx // | 存 +A8h
DWORD Ecx // | 器 +ACh
DWORD Eax //_|___组_ +B0h

DWORD Ebp //++++++ +B4h
DWORD Eip // |控制 +B8h
DWORD SegCs // |寄存 +BCh
DWORD EFlag // |器组 +C0h
DWORD Esp // | +C4h
DWORD SegSs //++++++ +C8h

BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;

下面是获取线程上下文的代码。

1
2
ctx.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(g_cpdi.hThead, &ctx);

像这样调用GetThreadContext() API,即可将指定线程的CONTEXT存储到ctx结构体变量。

1
2
3
4
5
6
BOOL GetThreadContext(
HANDLE hThread,
// handle of thread with context
LPCONTEXT lpContext
// address of context structure
);

3、获取WriteFile()的param2、3的值
调用WriteFile()函数时,我们要在传递过来的参数中知道param2(数据缓冲区地址)与param3(缓冲区大小)这两个参数。通过CONTEXT.Esp成员可以分别获得它们的值。

1
2
3
4
5
//获取WriteFile()的param 2、3值,param 2 = ESP + 0x8、param 3 = ESP + 0xC 

ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0x8), &dwAddrOfBuffer, sizeof(DWORD), NULL);

ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp +0xC), &dwNumOfBytesToWrite, sizeof(DWORD), NULL);

4、把小写字母转换为大写字母后覆写WriteFile()缓冲区
获取数据缓冲区的地址与大小后,将其内容读到调试器的内存空间,把小写字母转换为大写字母。然后将修改后的大写字母覆写到原位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//分配临时缓冲区
lpBuffer = (PBYTE)malloc(dwNumOfBytesToWrite+1);
memset(lpBuffer, 0, dwNumOfBytesToWrite+1);

//复制WriteFile()缓冲区到临时缓冲区
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer, lpBuffer, dwNumOfBytesToWrite, NULL);
printf("\n### original string ###\n%s\n", lpBuffer);

//将小写字母转换为大写字母
for( i = 0; i < dwNumOfBytesToWrite; i++ )
{
if( 0x61 <= lpBuffer[i] && lpBuffer[i] <= 0x7A )
lpBuffer[i] -= 0x20;
}

printf("\n### converted string ###\n%s\n", lpBuffer);

//将变换后的缓冲区复制到WriteFile()缓冲区
WriteProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer, lpBuffer, dwNumOfBytesToWrite, NULL);

//释放临时缓冲区
free(lpBuffer);

5、把线程上下文的EIP修改为WriteFile()起始地址
修改好CONTEXT.Eip成员后,调用SetThreadContext() API。

1
2
//将线程上下文的EIP更改为WriteFile()的首地址
ctx.Eip = (DWORD)g_pfWriteFile; SetThreadContext(g_cpdi.hThread, &ctx);

6、运行调试进程
调用ContinueDebugEvent() API可以重启进程,使之继续运行。
7、设置API“钩子”
最后设置API“钩子”,方便下次钩取操作。